5.1 - The Core of Control
Every command your bot issues—every attack, move, or build order—is an action that must be sent to the StarCraft II game engine. The functions that handle this critical transmission are built into the BotAI
class and its Unit
objects.
While there is a low-level self.do()
function, you will almost always use the high-level, convenient command methods like unit.attack()
or self.build()
. Understanding the two primary ways to execute these commands—individually or in a batch—is essential for writing an efficient bot.
The Action Pipeline: From Intent to Execution
When you want a unit to perform an action, your command goes through a simple pipeline.
+-----------------------------------+ (Stage 1) +----------------------+ (Stage 2) +--------------------------+
| Your Intent | ------------> | Creates UnitCommand | ------------> | self.do_actions(actions) |
| e.g., `my_marine.attack(target)` | | (Behind the scenes) | | (Sends to SC2) |
+-----------------------------------+ +----------------------+ +--------------------------+
(High-Level Method) (Intermediate Data Object) (Batch-Sending Engine)
-
Stage 1: High-Level Helper Methods: These are the readable methods on
Unit
andBotAI
objects, likeunit.attack()
,self.build()
, etc. When you call one, it creates aUnitCommand
data object that represents your intent. -
Stage 2: The
do_actions
Engine: Theawait self.do_actions()
function takes a list of thoseUnitCommand
objects and sends them all to the game engine in a single, efficient batch for execution. Awaiting a single action likeawait unit.attack()
is effectively a shortcut for creating and sending a batch of one.
This design gives you the best of both worlds: readable, high-level methods for creating actions and an efficient, low-level function for executing them in batches.
Individual vs. Batched Actions: A Performance Choice
You have two ways to execute actions. Your choice has significant performance implications, especially when commanding large armies.
Method | await unit.attack(...) (in a loop) | await self.do_actions([...]) |
---|---|---|
How It Works | Each await sends a command (or a micro-batch of one) to the game and waits for the game step to advance. | Collects many commands into a list and sends them all to the game in a single, efficient batch. |
Analogy | Making a separate phone call for every single instruction. | Sending one text message with a complete list of instructions. |
Pros | Very simple and intuitive for single actions. | Highly performant. Drastically reduces overhead when issuing many commands in one step. |
Cons | Inefficient for large groups. Can significantly slow down your on_step loop due to repeated await overhead. | Requires slightly more code (creating a list and appending to it). |
Best Practice: For any loop that issues commands to multiple units, always use the batched self.do_actions()
method.
A Developer's Checklist for Issuing Commands
- 1. Use High-Level Methods: Always use the convenient methods like
unit.attack()
orself.train()
to create your actions. - 2. Single Action? If you are only issuing one command,
await
it directly:await self.build(...)
. - 3. Multiple Actions? If you are looping through a
Units
collection:- Create an empty list:
actions = []
. - In the loop,
append
the command objects to the list withoutawait
. - After the loop,
await self.do_actions(actions)
.
- Create an empty list:
Code Example: Simple vs. Batched Performance
This bot demonstrates the two methods for commanding a group of idle Marines. You can comment out one block and run the other to see the difference. The batched method is the professional standard.
# batch_action_bot.py
from sc2 import maps
from sc2.bot_ai import BotAI
from sc2.data import Difficulty, Race
from sc2.main import run_game
from sc2.player import Bot, Computer
from sc2.ids.unit_typeid import UnitTypeId
from sc2.units import Units
class BatchActionBot(BotAI):
"""Demonstrates the performance difference between individual and batched actions."""
async def on_step(self, iteration: int):
# We need a target for our marines to attack.
target = self.enemy_start_locations[0].towards(self.start_location, 15)
# Find all idle marines.
idle_marines: Units = self.units(UnitTypeId.MARINE).idle
if not idle_marines.exists:
# If no idle marines, just build more and do nothing else this step.
await self.train_reinforcements()
return
# --- Method 1: Individual Commands (Simple but Inefficient) ---
# Each 'await' waits for the next game step.
# This is fine for 1-2 units, but very slow for 20+.
# for marine in idle_marines:
# await marine.attack(target)
# --- Method 2: Batched Commands (The Professional Standard) ---
# This is far more efficient as it sends all commands in one go.
actions = []
for marine in idle_marines:
# Create the command object and add it to our list.
actions.append(marine.attack(target))
# Send all commands at once.
await self.do_actions(actions)
async def train_reinforcements(self):
"""A simple method to build some prerequisite structures and train marines."""
if self.supply_left < 3 and self.already_pending(UnitTypeId.SUPPLYDEPOT) == 0:
if self.can_afford(UnitTypeId.SUPPLYDEPOT):
await self.build(UnitTypeId.SUPPLYDEPOT, near=self.start_location.towards(self.game_info.map_center, 5))
if self.structures(UnitTypeId.BARRACKS).amount < 2:
if self.can_afford(UnitTypeId.BARRACKS):
await self.build(UnitTypeId.BARRACKS, near=self.start_location.towards(self.game_info.map_center, 8))
if self.structures(UnitTypeId.BARRACKS).ready.idle.exists and self.can_afford(UnitTypeId.MARINE):
await self.train(UnitTypeId.MARINE)
if __name__ == "__main__":
run_game(
maps.get("BlackburnAIE"),
[
Bot(Race.Terran, BatchActionBot()),
Computer(Race.Zerg, Difficulty.Easy)
],
realtime=True,
)